--Leveling System API By: Genshin 

local base_exp = 1000 --The starting EXP
local rate = 1.5 --The rate of change for EXP tree progression
local default_level_cap = 99 --The level cap
local levelcolor = 15650457 --#EECE99
local expcolor = 7926527 --#78f2ff
local level_effect_height = 20
local levelup_msg_bg = nil --Hud ID
local levelup_msg_icon = nil --Hud ID
local levelup_msg = nil --Hud ID

--Global Functions - START

--Core Functions

--Get deserialized data from specified datastorage file
local get_table_data_from_world_directory = function(source)

  local file = io.open(minetest.get_worldpath().."/"..source..".txt", "r")
  local table = nil

  if file then
    local data = file:read("*all")
    if data == nil then
      data = {}
      file:write(minetest.serialize(data))
      file:close()
      return data
    else
      table = minetest.deserialize(data)
      file:close()
      return table
    end
  end

end


--Save data as serialized from specified datastorage file
local save_table_data_to_world_directory = function(source, input)

  local file = io.open(minetest.get_worldpath().."/"..tostring(source)..".txt", "w")

  if file then

    file:write(minetest.serialize(input))
    file:close()

  end

end

--Load player stats from database (Core)
local function load_list()
  local data = get_table_data_from_world_directory("player_levels")
  local result = {}

  if data == nil then

    data = {["player_levels"] = {}, ["level_cap"] = default_level_cap}

    result = data

    save_table_data_to_world_directory("player_levels", data)

  elseif next(data) == nil then
    data = {["player_levels"] = {}, ["level_cap"] = default_level_cap}

    result = data

    save_table_data_to_world_directory("player_levels", data)

  else

    result = data

  end

  return result
end


--Gets Max Level Value
function get_level_cap()
  local list = load_list()
  local result = tonumber(list["level_cap"]) or default_level_cap

  return result
end


--Sets Max Level Value
function set_level_cap(level)
  local list = load_list()

  list["level_cap"] = level

  save_table_data_to_world_directory("player_levels", list)
  return
end


--Display Level Up Message to Player
local function display_levelup_message(player)
  local pos = {x = 0.3, y = 0.1}
  local align = {x = 1, y = 0}
  local offset = {x = 60, y = 0}
  local size = {x = 0, y = 0}
  local level = get_player_level(player) or 1

  levelup_msg_bg = player:hud_add({
    hud_elem_type = "image",
    position      = pos,
    offset        = offset,
    text = "levelup_infobox_bg.png",
    alignment     = align,
    scale = {x = 1, y = 1},
  })

  levelup_msg_icon = player:hud_add({
    hud_elem_type = "image",
    position      = pos,
    offset        = {x = 150, y = 0},
    text = "levelup_infobox_Icon.png",
    alignment     = align,
    scale = {x = 1, y = 1},
  })

  levelup_msg = player:hud_add({
    hud_elem_type = "text",
    position      =  pos,
    offset        = {x = 250,   y = 0},
    text = "Congratulations! You are now Level "..level..".",
    number = 0xFFEF03,
    alignment     = align,
  })

  return
end


--Remove Level up message to player
local function remove_levelup_message(player)
  player:hud_remove(levelup_msg_bg)
  player:hud_remove(levelup_msg_icon)
  player:hud_remove(levelup_msg)
  return
end


--Get Player Level

function get_player_level(player)
  local name = player:get_player_name()
  local list = load_list()
  local result = 1

  if list["player_levels"][name] ~= nil then

    result = tonumber(list["player_levels"][name]["level"])

  end

  return result
end

--Set Player Level
function set_player_level(player, level)
  local name = player:get_player_name()
  local list = load_list()

  --Error Check
  if player == nil then
    error("Trying to set Level on nil.")
    return
  elseif player ~= nil and not player:is_player() then
    error("Player userdata expected, "..player.." is not a registered player!")
    return
  end

  if list["player_levels"][name] ~= nil then

    list["player_levels"][name]["level"] = tonumber(level)


    save_table_data_to_world_directory("player_levels", list)
  end


end

--Get Player EXP
function get_player_exp(player)
  local name = player:get_player_name()
  local list = load_list()
  local result = 0

  if list["player_levels"][name] == nil then
    return result

  else

    result = tonumber(list["player_levels"][name]["exp"])

  end

  return result
end

--Get Player Required EXP
function get_player_required_exp(player)
  local result = nil
  local name = player:get_player_name()
  local list = load_list()
  local level = get_player_level(player)

  result = tonumber(math.floor(base_exp * (level ^ rate)))
--Load player stats from database (Core)
function load_list()
  local data = get_table_data_from_world_directory("player_levels")
  local result = {}

  if data == nil then

    data = {["player_levels"] = {}, ["level_cap"] = default_level_cap}

    result = {}

    save_table_data_to_world_directory("player_levels", data)

  elseif next(data) == nil then
    data = {["player_levels"] = {}, ["level_cap"] = default_level_cap}

    result = data

    save_table_data_to_world_directory("player_levels", data)

  else

    result = data

  end

  return result
end


--Gets Max Level Value
function get_level_cap()
  local list = load_list()
  local result = tonumber(list["level_cap"]) or default_level_cap

  return result
end


--Sets Max Level Value
function set_level_cap(level)
  local list = load_list()

  list["level_cap"] = level

  save_table_data_to_world_directory("player_levels", list)
  return
end


--Get Player Level

function get_player_level(player)
  local name = player:get_player_name()
  local list = load_list()
  local result = 1

  if list["player_levels"][name] ~= nil then

    result = tonumber(list["player_levels"][name]["level"])

  end

  return result
end

--Set Player Level
function set_player_level(player, level)
  local name = player:get_player_name()
  local list = load_list()

  --Error Check
  if player == nil then
    error("Trying to set Level on nil.")
    return
  elseif player ~= nil and not player:is_player() then
    error("Player userdata expected, "..player.." is not a registered player!")
    return
  end

  if list["player_levels"][name] ~= nil then

    list["player_levels"][name]["level"] = tonumber(level)


    save_table_data_to_world_directory("player_levels", list)
  end


end

--Get Player EXP
function get_player_exp(player)
  local name = player:get_player_name()
  local list = load_list()
  local result = 0

  if list["player_levels"][name] == nil then
    return result

  else

    result = tonumber(list["player_levels"][name]["exp"])

  end

  return result
end

--Get Player Required EXP
function get_player_required_exp(player)
  local result = nil
  local name = player:get_player_name()
  local list = load_list()
  local level = get_player_level(player)

  result = tonumber(math.floor(base_exp * (level ^ rate)))

  return result
end

--Add Player EXP
function add_player_exp(player, exp)
  local name = player:get_player_name()
  local current_exp = get_player_exp(player)
  local list = load_list()
  local level = get_player_level(player)
  local level_cap = get_level_cap()
  local required_exp = get_player_required_exp(player)

  --Error Check
  if player == nil then
    error("Trying to add EXP on nil.")
    return
  elseif player ~= nil and not player:is_player() then
    error("Player userdata expected, "..player.." is not a registered Player!")
    return
  end

  if list["player_levels"][name] ~= nil then

    --Do not add any more EXP if level reached maximum or beyond
    if level >= level_cap then
      return
    end

    --Avoid overflow for hud
    if exp >= required_exp then
      exp = required_exp
    end

    list["player_levels"][name]["exp"] = current_exp + tonumber(exp)

    save_table_data_to_world_directory("player_levels", list)
  end

  if minetest.get_modpath("stats_core") then
  end

end

--Set Player EXP
function set_player_exp(player, exp)
  local name = player:get_player_name()
  local list = load_list()
  local required_exp = get_player_required_exp(player)

  --Error Check
  if player == nil then
    error("Trying to set EXP on nil.")
    return
  elseif player ~= nil and not player:is_player() then
    error("Player userdata expected, "..player.." is not a registered Player!")
    return
  end

  if list["player_levels"][name] ~= nil then

    --Avoid overflow for hud
    if exp >= required_exp then
      exp = required_exp
    end

    list["player_levels"][name]["exp"] = tonumber(exp)


    save_table_data_to_world_directory("player_levels", list)
  end

end
  return result
end

--Add Player EXP
function add_player_exp(player, exp)
  local name = player:get_player_name()
  local current_exp = get_player_exp(player)
  local list = load_list()
  local level = get_player_level(player)
  local level_cap = get_level_cap()
  local required_exp = get_player_required_exp(player)

  --Error Check
  if player == nil then
    error("Trying to add EXP on nil.")
    return
  elseif player ~= nil and not player:is_player() then
    error("Player userdata expected, "..player.." is not a registered Player!")
    return
  end

  if list["player_levels"][name] ~= nil then

    --Do not add any more EXP if level reached maximum or beyond
    if level >= level_cap then
      return
    end

    --Avoid overflow for hud
    if exp >= required_exp then
      exp = required_exp
    end

    list["player_levels"][name]["exp"] = current_exp + tonumber(exp)

    save_table_data_to_world_directory("player_levels", list)
  end

  if minetest.get_modpath("stats_core") then
  end

end

--Set Player EXP
function set_player_exp(player, exp)
  local name = player:get_player_name()
  local list = load_list()
  local required_exp = get_player_required_exp(player)

  --Error Check
  if player == nil then
    error("Trying to set EXP on nil.")
    return
  elseif player ~= nil and not player:is_player() then
    error("Player userdata expected, "..player.." is not a registered Player!")
    return
  end

  if list["player_levels"][name] ~= nil then

    --Avoid overflow for hud
    if exp >= required_exp then
      exp = required_exp
    end

    list["player_levels"][name]["exp"] = tonumber(exp)


    save_table_data_to_world_directory("player_levels", list)
  end

end


--Icon Stuff - START
local level_up_activate = function(self, staticdata, dtime_s, def)

  -- load entity variables
  local tmp = minetest.deserialize(staticdata)

  if tmp then

    for _,stat in pairs(tmp) do
      self[_] = stat
    end
  end


end


local level_up_step = function(self)
  local pos = self.object:get_pos()
  local player = self.wielder
  local is_attached = self.object:get_attach()

  --Verify if this Icon is attached to a player, if it's not attached at all, remove it.
  if is_attached == nil then
    self.object:remove()
  else
    self.attached = true
  end

  --If attached to a player, do the following
  if self.attached == true then
    self.timer = self.timer - 1
    self.aura_timer = self.aura_timer - 1

    if self.aura_timer > 0 then

      minetest.add_particlespawner({
        amount = 2,
        time = 0.25,
        minpos = pos,
        maxpos = pos,
        minvel = {x = -6, y = -6, z = -6},
        maxvel = {x = 6, y = 6, z = 6},
        minacc = {x = 0, y = 15, z = 0},
        maxacc = {x = 0, y = 15, z = 0},
        minexptime = 0.1,
        maxexptime = 1,
        minsize = math.random(3,7),
        maxsize = math.random(7,12),
        texture = "levelup_aura.png",
        collisiondetection = true,
        vertical = false,
        glow = 10,
      })

    end

    if self.timer <= 0 then
      remove_levelup_message(player)
      self.object:remove()
    end

  end

end


--Levelup Icon That Appears around the player
minetest.register_entity("player_levels:level_up_effect", {
  physical = false,
  collisionbox = {x=0, y=0, z=0},
  visual = "sprite",
  glow = 100,
  textures = {"levelup_placeholder.png"},
  wielder = nil,
  attached = false,
  on_activate = level_up_activate,
  on_step = level_up_step,
  glow = 30,
  aura_timer = 20,
  timer = 100
})


--Icon Stuff - END


--check current level stats
function check_player_current_level(player)

  print(player)

  local name = player:get_player_name()
  local list = load_list()
  local exp = get_player_exp(player)
  local level = get_player_level(player)
  local required_exp = get_player_required_exp(player)
  local level_cap = get_level_cap()

  if list["player_levels"][name] ~= nil then

    list["player_levels"][name]["exp"] = exp


    save_table_data_to_world_directory("player_levels", list)

    --Do Level UP!
    if level >= level_cap then
      return
    else

      if exp >= required_exp then
        local next_level = level + 1

        set_player_level(player, next_level)
        set_player_exp(player, 0)

        minetest.sound_play("level_up", {
          to_player = player:get_player_name(),
          gain = 2.0,
        })

        local pos = player:get_pos()

        display_levelup_message(player)



        local levelup_icon = minetest.env:add_entity(pos, "player_levels:level_up_effect")

        levelup_icon:set_attach(player, "", {x = 0, y = level_effect_height, z = 0}, {x = 0, y = 0, z = 0})

        local ent = levelup_icon:get_luaentity()
        ent.wielder = player
        ent.attached = true

      end

    end

    print("\n[DEBUG]: Level = "..tostring(get_player_level(player))..", EXP = "..tostring(get_player_exp(player)).."!\n")

  end

end


--Global Functions - END


--Check if data exists for a player that just logged in
minetest.register_on_joinplayer(function(player)
  local list = load_list()
  local name = player:get_player_name()
  local to_generate = nil
  local exp = nil
  local level = nil
  local level_cap = get_level_cap()


  --make sure this player has a level, if not then register this player's level to it's database
  if list["player_levels"][name] == nil then

    list["player_levels"][name] = {
      level = 1,
      exp = 0
    }

    save_table_data_to_world_directory("player_levels", list)
  end

  --Fetch old variables and Clear them if exists
  if player:get_attribute("player_level") ~= nil
  and player:get_attribute("exp") ~= nil
  then

    level = tonumber(player:get_attribute("player_level"))
    exp = tonumber(player:get_attribute("exp"))

    set_player_level(player, level)
    set_player_exp(player, exp)

    player:set_attribute("player_level", nil)
    player:set_attribute("exp", nil)
    player:set_attribute("required_exp", nil)

  else

    --Generate level the default way
    level = get_player_level(player)
    exp = get_player_exp(player)

  end

end)

print("[ADDON]: Player Levels API started Successfully!")